import pygame # 导入 pygame 库，这是制作游戏的基础
import random # 导入 random 库，用于生成随机数，比如 AI 在没有目标时随机移动
from .snake import Snake # 从当前目录下的 snake.py 文件中导入 Snake 类，AI蛇是基于普通蛇的，所以需要继承它
from .constant import SNAKE_SKINS  # 导入皮肤配置,从当前目录下的 constant.py 文件中导入 SNAKE_SKINS 字典，这里面定义了所有蛇的皮肤颜色
import math # 导入 math 库，用于进行数学计算，比如计算两点之间的直线距离

# 定义一个名为 AISnake 的类，它继承自之前导入的 Snake 类
class AISnake(Snake):
    """
    人机蛇类，继承自普通的Snake类，拥有独立的AI决策逻辑。
    """

    # 类的初始化方法（构造函数），当创建一个新的 AISnake 对象时，这个方法会被自动调用
    def __init__(self, base_dir, skin_id, screen_w, screen_h, start_pos=None, start_dir=None, initial_length=4):
        # 调用父类（Snake类）的初始化方法，这样 AISnake 就能拥有 Snake 类的所有基本属性和功能
        """
        “调用我的父类(Snake)的 __init__ 方法”
        "并且把 base_dir, skin_id 这些参数传给它，让它完成基础初始化"
        """
        super().__init__(base_dir, skin_id, start_pos, start_dir, initial_length=initial_length)
        # --- 父类的工作到此完成 ---
        # --- 现在开始子类 (AISnake) 自己的改装工作 ---
        """
        1.super() 就是一个让你能够调用父类方法的特殊工具
        """
        # 存储屏幕的宽度，用于后续判断边界
        self.screen_w = screen_w
        # 存储屏幕的高度，用于后续判断边界
        self.screen_h = screen_h
        # 设置一个标志，表明这是一个 AI 蛇，方便在主逻辑中区分玩家和 AI
        self.is_ai = True
        # 设置 AI 蛇的基础移动速度
        self.base_speed = 7
        # 设置 AI 蛇的初始长度
        self.initial_length = initial_length

        # --- 新增: 为AI蛇添加独立的分数属性 ---
        self.score = 0

    # 定义一个方法，用来获取当前所有安全的移动方向
    def get_safe_directions(self, obstacles, all_bodies):
        # 获取蛇头的当前位置（它是一个pygame.Rect对象）
        head = self.body[0]
        # 创建一个空列表，用来存放计算后得出的安全方向
        safe_moves = []
        # 创建一个字典，存储四个基本方向键及其对应的坐标变化量 (x, y)
        possible_moves = {pygame.K_UP: (0, -25), pygame.K_DOWN: (0, 25), pygame.K_LEFT: (-25, 0),
                          pygame.K_RIGHT: (25, 0)}
        # 创建一个字典，存储每个方向的反方向，防止蛇掉头
        opposites = {pygame.K_UP: pygame.K_DOWN, pygame.K_DOWN: pygame.K_UP, pygame.K_LEFT: pygame.K_RIGHT,
                     pygame.K_RIGHT: pygame.K_LEFT}
        # 遍历所有可能的移动方向
        for move_key, (dx, dy) in possible_moves.items():
            """
            1.possible_moves 是一个字典：{pygame.K_UP: (0, -25), ...}
            2.  .items() 会把字典里的“键”和“值”一对一对地取出来。
            键 (key) 是：pygame.K_UP, pygame.K_DOWN 等...
            值 (value) 是：(0, -25), (0, 25) 等...
            3.for 循环在遍历时，会把“键”赋值给 move_key，把“值”赋值给 (dx, dy)
            4.把 (0, -25) 这样的元组（tuple）直接赋给 (dx, dy) 的操作叫做 “元组解包” 
            5.(dx, dy) 就是你预先定义好的坐标变化量：
            dx 代表 "delta x" (X方向的变化量)
            dy 代表 "delta y" (Y方向的变化量)
            (0, -25) 就代表：X不变，Y减25（向上移动）。
            """
            # 如果这个方向是当前方向的反方向，就跳过，因为蛇不能直接掉头
            #防止蛇 180 度掉头
            if self.dirction == opposites.get(move_key): continue
            # 模拟移动一步，计算出蛇头将要到达的下一个位置
            next_pos = head.move(dx, dy)
            """
            head：是蛇头的 pygame.Rect 对象，代表蛇头现在的位置。
            .move(dx, dy)：这是 pygame.Rect 的一个方法。它不会移动原来的 head 对象。
            它的作用是：返回一个全新的 Rect 对象，这个新对象的位置是在 head 的基础上，加上了 (dx, dy) 的偏移量.
            next_pos：这个变量就存储了那个假想的、模拟的“下一步位置”
            """
            if next_pos not in all_bodies and not any(next_pos.colliderect(obs['rect']) for obs in obstacles):
                """
                一共有两个条件，
                条件一：next_pos not in all_bodies
                all_bodies：是一个列表，包含了所有蛇的身体方块（Rect 对象），包括它自己的身体。
                not in：检查 next_pos（那个“假设的位置”）是否不在 all_bodies 列表里。
                含义：检查“我下一步要走的地方，是不是已经有蛇的身体了？”。
                目的：防止 AI 蛇咬到自己或另一条 AI 蛇的身体。
                条件二：and not any(next_pos.colliderect(obs['rect']) for obs in obstacles)
                1.for obs in obstacles：遍历 obstacles（障碍物）列表中的每一项。
                2.obs['rect'] 就是取出这个障碍物的 pygame.Rect 碰撞箱。
                3.next_pos.colliderect(obs['rect'])：
                它检查 next_pos（“假设的位置”）是否和当前的这个障碍物 obs['rect'] 发生了碰撞。
                如果撞了，返回 True；没撞，返回 False。
                4.( ... for obs in obstacles)：
                这是一个“列表推导式”或“生成器表达式”。它会对每一个障碍物都执行一次 colliderect 检查。
                结果：你会得到一个包含很多 True 或 False 的“结果列表”。
                例如：[False, False, True, False] (表示撞到了第3个障碍物)
                5.any(...)：
                any() 函数会检查这个“结果列表”。只要列表里有至少一个 True，any() 就会返回 True。
                6.not any(...)：not 会把 any() 的结果反过来。
                如果 any() 是 True（撞到了），not any() 就是 False（不安全）。
                如果 any() 是 False（没撞到），not any() 就是 True（安全）。
                """
                safe_moves.append(move_key)
                """if 语句里的两个条件都满足了（都是 True），就说明 move_key (比如 pygame.K_UP) 是一个绝对安全的方向。
                safe_moves.append(move_key)：就把这个安全的方向（move_key）添加到一个叫 safe_moves 的列表里"""
        return safe_moves

    ## --- 修改标记 2：彻底重写AI决策函数，植入“逃跑”与“追击”逻辑 --- ##
    def update_direction(self, normal_food_pos, special_food_pos, obstacles, player_snake_body, all_bodies):
        """
        AI决策核心，遵循三层思考逻辑：
        1. 生存优先：如果玩家比我强且离我很近，立刻进入“逃跑模式”。
        2. 攻击其次：如果我比玩家强，在“追击玩家”和“吃最近的食物”之间做出最优选择。
        3. 觅食保底：在没有威胁和机会时，安心寻找最近的食物。
        """
        # --- 第零步：信息收集 ---
        # 获取 AI 蛇头的中心点坐标
        my_head_pos = self.body[0].center
        # 获取 AI 蛇的当前长度
        my_length = len(self.body)
        # 获取玩家蛇头的中心点坐标
        player_head_pos = player_snake_body[0].center
        # 获取玩家蛇的当前长度
        player_length = len(player_snake_body)
        # 使用 math.hypot 计算 AI 蛇头与玩家蛇头之间的直线距离（勾股定理）
        distance_to_player = math.hypot(player_head_pos[0] - my_head_pos[0], player_head_pos[1] - my_head_pos[1])
        """
        1.player_head_pos：玩家蛇头的位置。它是一个坐标，my_head_pos：AI 蛇头的位置。它也是一个坐标，
        2.要计算 d，根据勾股定理（a^2 + b^2 = c^2），我们必须先知道直角三角形的两条短边 a 和 b的长度。
        3.边 a (水平距离 Delta x)：这就是 player_head_pos[0] - my_head_pos[0] 玩家的 X 坐标 - AI 的 X 坐标
        边 b(垂直距离 Delta y)：这就是 player_head_pos[1] - my_head_pos[1] 玩家的 Y 坐标 - AI 的 Y 坐标
        4.math.hypot() 是 Python math 库里的一个函数,math.hypot(a, b) 会自动帮你计算 c = sqrt{a^2 + b^2}
        """
        # 定义一个“危险距离”，当玩家进入这个范围时，AI会开始警觉。
        THREAT_DISTANCE = 150  # 大约6个格子的距离

        # --- 第一层思考：生存 (Fleeing) ---
        # 如果我比玩家短或一样长，并且玩家已经进入了我的危险范围
        if my_length <= player_length and distance_to_player < THREAT_DISTANCE:
            # 进入“逃跑模式”！
            # 首先，获取所有安全的移动方向
            safe_moves = self.get_safe_directions(obstacles, all_bodies)
            if not safe_moves: return  # 如果无路可逃，就听天由命

            # 计算哪个安全方向能让我离玩家最远
            # 创建一个空字典，用来存放每个安全方向能让我离玩家多远
            flee_distances = {}
            for move in safe_moves:
                # 模拟移动一步
                # 根据方向键（move），从字典中查出对应的坐标变化 (dx, dy)
                """
                1.是一个非常简洁的**“查字典”**快捷方式，它的作用是：根据 move 变量中存储的“方向”，立刻查出对应的坐标变化量 (dx, dy)
                2.第1步：创建“方向字典”,
                键 (Key)：是方向键，比如 pygame.K_UP (上), pygame.K_DOWN (下) 等。
                值 (Value)：是方向键对应的坐标变化元组（tuple）
                3.第2步：使用 [move] 查字典，字典[键] 这种语法就是用来根据“键”来获取它对应的“值”
                4.使用 dx, dy = ... 进行“元组解包”
                dx, dy = (0, -25) 这个操作叫做 “元组解包”
                看到等号右边是一个有两个元素的元组 (0, -25)，等号左边是两个变量 dx, dy，它就会自动地、按顺序地把元组里的值赋给左边的变量。
                0 (第一个元素) 被赋值给 dx。      -25 (第二个元素) 被赋值给 dy。
                """
                dx, dy = \
                {pygame.K_UP: (0, -25), pygame.K_DOWN: (0, 25), pygame.K_LEFT: (-25, 0), pygame.K_RIGHT: (25, 0)}[
                    move]
                """
                1.字典[X] 就是“按键查值”
                2.\ 在 Python 中叫做 “续行符”,它告诉 Python 解释器：“这一行代码还没写完，请继续阅读下一行，把下一行当作这一行代码的延续。”
                Python 通常认为“换行”（按一下 Enter 键）就代表一行代码的结束。
                """
                # 计算出移动后的新位置坐标
                next_pos = (my_head_pos[0] + dx, my_head_pos[1] + dy)
                # 计算移动后与玩家的距离
                flee_distances[move] = math.hypot(next_pos[0] - player_head_pos[0],
                                                  next_pos[1] - player_head_pos[1])

            # 选择能让距离变得最远的那个方向作为逃跑路线
            # 如果 flee_distances 字典不是空的（即有路可逃）
            if flee_distances:
                # 找出能让距离变得最远的那个方向
                best_flee_move = max(flee_distances, key=flee_distances.get)
                """
                max 是 Python 的一个内置函数，它的基本作用是从一个集合中找出“最大”的那个元素。
                1.max 函数的**“比较规则”**。
                当你只用 max(flee_distances) 时，max 会比较字典的键 ('up', 'down', 'left', 'right')，它会按字母表顺序返回 'up'
                2.key=flee_distances.get 告诉 max 函数：“嘿！不要比较键本身！
                在比较时，请使用 flee_distances.get 这个方法去获取每个键对应的‘值’，然后用那些‘值’来比较。”
                3.找到最大的值之后，不会返回值而是返回键
                """
                # 将蛇的移动方向设置为这个最佳逃跑方向
                self.dirction = best_flee_move
                # 决策完成，立刻返回，不再考虑后面的吃食物等逻辑
                return  # 决策完成，保命要紧，不再考虑食物

        # --- 如果不需要逃跑，则进入第二/三层思考：攻击 (Hunting) 或 觅食 (Eating) ---

        # 1. 找出当前场上所有可见的食物
        # 创建一个空列表，用来存放所有食物目标
        targets = []
        # 如果普通食物存在（坐标不是-25），就把它加入目标列表
        if normal_food_pos.x != -25: targets.append(normal_food_pos)
        # 如果特殊食物存在（坐标不是-25），也把它加入目标列表
        if special_food_pos.x != -25: targets.append(special_food_pos)

        # 2. 找出距离最近的食物作为“觅食”备选目标
        # 初始化最近食物目标为 None（空）
        best_food_target = None
        # 如果目标列表里有食物
        if targets:
            # 使用 min 函数找到列表中离 AI 蛇头最近的那个食物
            # key=lambda t: ... 的意思是，比较列表里每个元素 t 时，不是比较 t 本身，而是比较 t 计算后的一个值
            # 这里计算的值就是 AI 蛇头到食物 t 中心的距离
            # 计算到每个食物的距离，并找到最近的那个
            best_food_target = min(targets,
                                   key=lambda t: math.hypot(t.centerx - my_head_pos[0], t.centery - my_head_pos[1]))
        """
        min() 是 Python 的一个内置函数，它的基础作用是从一个列表（或其它可迭代对象）里找出“最小”的那个元素。
        1.给 min() 提供一个“比较的规则”，告诉它到底该比较什么。这个规则就是 key 参数
        2.lambda 关键字用来创建一个匿名的、临时的、单行的小函数。
        lambda：表示“我要创建一个临时小函数”。
        t：给这个函数的参数起个名字，叫 t。当 min() 遍历 targets 列表时，它会把列表里的每一个食物对象依次放进这个 t 里面。
        : 后面的部分,这是这个临时小函数的函数体
        3.min返回的也是键，不是值
        4.t 只是一个临时的占位符，它是 lambda 函数的参数，不会把每次算出来的值赋给t,例如，t先是foodA，
        算完值，min函数有内部记忆，会记录，如果之后没有比这个值小的值，min函数的值不改变，
        如果后续有比这个值还小的值，该值会覆盖掉min函数之前记录的值
        循环结束后，会找到这个值对应的键，返回这个键
        """
        # 3. 做出最终决策：追玩家还是吃食物？
        # 初始化最终选择的目标为 None（空）
        chosen_target = None
        # 如果我比玩家长，我有攻击资格
        if my_length > player_length:
            distance_to_best_food = float('inf')  # 先假设食物无限远
            """
            float:浮点数，
            inf指的是无穷大
            float('inf'):
            这行代码的作用就是，在 Python 中创建一个数字，这个数字代表“正无穷大” (∞)
            """
            # 如果真的找到了最近的食物
            if best_food_target:
                # 就计算出到这个食物的实际距离
                distance_to_best_food = math.hypot(best_food_target.centerx - my_head_pos[0],
                                                   best_food_target.centery - my_head_pos[1])

            # 如果玩家比最近的食物还要近，那么“追击玩家”就是更好的选择！
            if distance_to_player < distance_to_best_food:
                chosen_target = player_snake_body[0]  # 目标是玩家的头
            else:
                chosen_target = best_food_target  # 否则还是先吃食物
        else:
            # 如果我没有攻击资格（比玩家短或一样长），那我只能老老实实吃食物
            chosen_target = best_food_target

        # --- 第四步：执行移动 ---
        # 如果最终没有可选目标（比如场上没食物，玩家也比我强），就安全地随机移动
        if chosen_target is None:
            # 就获取所有安全的方向
            safe_moves = self.get_safe_directions(obstacles, all_bodies)
            # 如果有安全的方向，就从中随机选一个移动
            if safe_moves: self.dirction = random.choice(safe_moves)
            # 决策完成，返回
            return

        # 朝着最终选定的目标（chosen_target）前进 (这部分是原来的觅食逻辑)
        # 获取目标的中心坐标
        target_center = chosen_target.center
        # 计算蛇头和目标在 x 和 y 轴上的差距
        dx, dy = target_center[0] - my_head_pos[0], target_center[1] - my_head_pos[1]
        #my_head 这里指AI蛇
        # 创建一个列表，存放优先考虑的移动方向
        preferred_moves = []
        # 如果水平差距大于垂直差距，说明目标主要在左右方向
        if abs(dx) > abs(dy):
            # 如果目标在右边，优先向右
            if dx > 0:
                preferred_moves.append(pygame.K_RIGHT)
            # 如果目标在左边，优先向左
            else:
                preferred_moves.append(pygame.K_LEFT)
            # 然后再考虑次要的上下方向
            if dy > 0:
                preferred_moves.append(pygame.K_DOWN)
            else:
                preferred_moves.append(pygame.K_UP)
        # 否则，说明目标主要在上下方向
        else:
            # 如果目标在下边，优先向下
            if dy > 0:
                preferred_moves.append(pygame.K_DOWN)
            # 如果目标在上边，优先向上
            else:
                preferred_moves.append(pygame.K_UP)
            # 然后再考虑次要的左右方向
            if dx > 0:
                preferred_moves.append(pygame.K_RIGHT)
            else:
                preferred_moves.append(pygame.K_LEFT)
        # 再次获取所有当前安全的方向
        safe_moves = self.get_safe_directions(obstacles, all_bodies)
        # 如果没有安全的路，就返回，不动
        if not safe_moves: return
        # 遍历我们优先想走的方向
        for move in preferred_moves:
            # 如果这个优先方向是安全的
            if move in safe_moves:
                # 就把蛇的方向设为这个方向
                self.dirction = move
                # 决策完成，返回
                return

        # 如果所有优先方向都不安全，就在所有安全方向里随便选一个
        self.dirction = random.choice(safe_moves)

    def move(self):
        super().move()
        #继承父类的移动
        head = self.body[0]
        if head.left < 0:
            head.left = self.screen_w - 25
        elif head.right > self.screen_w:
            head.left = 0
        if head.top < 0:
            head.top = self.screen_h - 25
        elif head.bottom > self.screen_h:
            head.top = 0

    def reset(self, start_pos=None, start_dir=None, skin_id=None):
        """ 重置AI蛇的状态（位置、方向、皮肤、长度和分数）。 """
        # 如果传入了新的皮肤ID
        if skin_id:
            # 就从皮肤字典 SNAKE_SKINS 中获取对应的颜色，并更新自己的皮肤
            self.skin_colors = SNAKE_SKINS.get(skin_id, SNAKE_SKINS['default'])['colors']
        """
        SNAKE_SKINS['default']这是一个标准的字典查找。
        1.它的意思是：“从 SNAKE_SKINS 字典里，把键 (key) 为 'default' 的那个值 (value) 拿出来。”
        2..get() 是 Python 字典一个非常安全的查找方法。它和 字典[键] 的区别在于：
        字典[键]：如果“键”不存在，程序会崩溃报错。
        字典.get(键, 默认值)：如果“键”不存在，它不会报错，而是会返回你提供的“默认值”。
        3.SNAKE_SKINS.get(skin_id, SNAKE_SKINS['default']) 的意思是： “请尝试用 skin_id 作为键去 SNAKE_SKINS 里查找。”
        如果找到了（比如 skin_id 是 'fire'），那么 .get() 方法就会返回 'fire' 对应的那个字典
        4.[...] ['colors']
        [...] 代表第2步返回的那个“皮肤信息字典”。
        在那个字典后面再跟上 ['colors']，意思就很简单了：“从刚刚得到的那个‘皮肤信息字典’里，把键为 'colors' 的值拿出来
        5.self.skin_colors = ...这就是一个简单的赋值操作。
        把第3步最终得到的颜色元组（比如 ('#FF0000', '#FFA500')）赋值给 self.skin_colors 这个实例属性
        """
        # 清空蛇的身体列表
        self.body = []
        # 设置蛇的初始方向，如果有指定就用指定的，没有就默认向右
        self.dirction = start_dir if start_dir is not None else pygame.K_RIGHT
        """
        条件表达式:
        variable = [如果为真的值] if [判断条件] else [如果为假的值]
        第一个表达式（设置方向）：
        1.[判断条件]: start_dir is not None
        第二个表达式（设置位置）：
        1.[判断条件]: start_pos is not None
        """
        # 设置蛇的初始位置，如果有指定就用指定的，没有就随机生成一个
        initial_pos = start_pos if start_pos is not None else (random.randint(4, 30) * 25, random.randint(4, 30) * 25)
        """
        1.random.randint(4, 30)  random.randint(a, b) 是 Python 的一个函数，它的作用是：
        “在 a 和 b 之间（包括 a 和 b 本身），随机挑选一个整数（整数）。”
        2.... * 25,（贪吃蛇）是在一个“网格”上移动的。你的蛇每走一步，很可能就是移动 25 个像素。
        所以，蛇的 X 和 Y 坐标必须是 25 的倍数，它才能“对齐”在网格上。
        3.(4, 30) 这个范围的意思是，蛇不会出生在太靠近屏幕边缘（0-3格）的地方。
        4.(..., ...)   最后，() 括号把两个独立计算出来的随机坐标组合成一个元组 (tuple)。
        initial_pos = (随机生成的X坐标, 随机生成的Y坐标)
        """

        # --- 新增: 重生时分数也清零 ---
        # 将 AI 的分数重置为 0
        self.score = 0

        # 根据初始长度，循环创建蛇的身体节点
        for i in range(self.initial_length):
            """
            1.for i in range(self.initial_length):
            假设 self.initial_length 是 4，这个循环会执行 4 次，i 的值会依次是：
            i = 0 (用来创建蛇头)
            i = 1 (用来创建第1节身体)
            i = 2 (用来创建第2节身体)
            i = 3 (用来创建第3节身体)
            i 在这里代表“这是第几个身体方块”
            """
            # 创建一个身体节点（一个矩形）
            node = pygame.Rect(initial_pos[0], initial_pos[1], 25, 25)
            """
            1.node = pygame.Rect(initial_pos[0], initial_pos[1], 25, 25)
            这是第一个巧妙的地方：在每一次循环中，我们都先在同一个位置 initial_pos (比如 (100, 100)) 创建一个方块。
            你把 4 块砖头全都扔在了同一个点上，它们现在是堆叠在一起的。
            2.if/elif 调整位置 (最关键)
            接下来，代码会根据蛇头朝向，把这堆砖头（方块）“推”到各自正确的位置上，让它们排成一队
            K_RIGHT (向右) 之所以要用 node.left -= ... (向左减)，是因为我们以“蛇头”为基准点 (i=0)，
            去“向后”（与前进方向相反）排列它的身体。头在右边，身体在左边
            K_LEFT (向左) 就要用 node.left += ... (向右加)，因为身体在头的右边。
            K_UP (向上) 就要用 node.top += ... (向下加)，因为身体在头的下边。
            K_DOWN (向下) 就要用 node.top -= ... (向上减)，因为身体在头的上边
            """
            # 根据初始方向，调整每个身体节点的初始位置，让它们排成一条线
            if self.dirction == pygame.K_LEFT:
                node.left += i * 25
            elif self.dirction == pygame.K_RIGHT:
                node.left -= i * 25
            elif self.dirction == pygame.K_UP:
                node.top += i * 25
            elif self.dirction == pygame.K_DOWN:
                node.top -= i * 25
            # 将创建好的节点添加到身体列表中
            self.body.append(node)